package dmxGroupControl.solvers;
import java.util.*;
import java.util.Map.Entry;

import dmxGroupControl.LightController;
import org.jdom2.Attribute;

import dmxGroupControl.Fixture;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import org.jdom2.Element;

public class LookAtSolver extends Solver
{
    private static List<String> solvableChannels = Arrays.asList("Pan", "Tilt");
    private static List<String> solvableAttributes = Arrays.asList("LookAt");
    
	private float lookAtTargetValue;
	private Point3D source;
	private boolean inverted;
	private Fixture fixture;
	
	private double tiltMin, tiltMax, panMin, panMax;
	
	private TreeMap<Float, Point3D> lookAtTargets;
	
	public LookAtSolver(double x, double y, double z, Fixture fixture)
	{
        Element definitionElement = LightController.GetFixtureDefinition(fixture.GetName());

        double panMin = 0.0f, panMax = 0.0f, tiltMin = 0.0f, tiltMax = 0.0f;
        for (Element movementElement : definitionElement.getChildren("movement"))
        {
            switch (movementElement.getAttributeValue("type"))
            {
                case "Pan":
                    panMin = Double.parseDouble(movementElement.getAttributeValue("min"));
                    panMax = Double.parseDouble(movementElement.getAttributeValue("max"));
                    break;
                case "Tilt":
                    tiltMin = Double.parseDouble(movementElement.getAttributeValue("min"));
                    tiltMax = Double.parseDouble(movementElement.getAttributeValue("max"));
                    break;
            }
        }

        lookAtTargetValue = 0.0f;
		this.fixture = fixture;
		source = new Point3D(x, y, z);
		this.panMin = panMin;
		this.panMax = panMax;
		this.tiltMin = tiltMin;
		this.tiltMax = tiltMax;
		lookAtTargets = new TreeMap<>();
		
		Point3D neutral;
		if(y > 0)
		{
			//placed upward, facing down
			inverted = true;
			neutral = new Point3D(x, 0, z);
		}
		else
		{
			inverted = false;
			neutral = new Point3D(x, 1, z);
		}
		
		lookAtTargets.put(0.0f, neutral);
		lookAtTargets.put(16.0f / 127.0f, new Point3D(x, 0, -z));
		lookAtTargets.put(32.0f / 127.0f, new Point3D(0, 0, -z));
		
		fixture.GetChannels().OptionalSetChannel("Pan", 0.5f);
		fixture.GetChannels().OptionalSetChannel("Tilt", 0.5f);
		
		lookAtTargets.put(1.0f, neutral);
	}

	@Override
	public void SolveInternal()
	{
		Point3D target = GetTarget();
		Point3D relativeTarget = target.subtract(source);
		
		double pan, tilt;
		
		Point2D forward = new Point2D(0, 1);
		Point2D panAngle = new Point2D(relativeTarget.getX(), relativeTarget.getZ());
		if(Point2D.ZERO.equals(panAngle))
		{
		    pan = 0;
		}
		else
		{
		    pan = forward.angle(panAngle);
		}
		
		Point2D up;
		if(inverted)
		{
		    up = new Point2D(0, -1);
		}
		else
		{
		    up = new Point2D(0, 1);
		}
		
		Point2D tiltAngle = new Point2D(relativeTarget.getZ(), relativeTarget.getY());
		tilt = up.angle(tiltAngle);
		
		double tiltRange = tiltMax - tiltMin;
		double panRange = panMax - panMin;
		
		if(fixture.GetChannels().Contains("Pan") && fixture.GetChannels().Contains("Tilt"))
        {
            TreeMap<Double, Double> alternative = GetAlternatives(pan, tilt);
            double currentPan = fixture.GetChannels().GetChannel("Pan").GetValue() * panRange + panMin;
            double currentTilt = fixture.GetChannels().GetChannel("Tilt").GetValue() * tiltRange + tiltMin;
            double currentDifference = GetDifference(currentPan, currentTilt, pan, tilt);
            
            for(Entry<Double, Double> current : alternative.entrySet())
            {
                double totalDifference = GetDifference(currentPan, currentTilt, current.getKey(), current.getValue());
                if(totalDifference < currentDifference)
                {
                    currentDifference = totalDifference;
                    pan = current.getKey();
                    tilt = current.getValue();
                }
            }
        }
		
		double relativeTilt = (tilt - tiltMin) / tiltRange;
        double relativePan = (pan - panMin) / panRange;
		
        fixture.GetChannels().OptionalSetChannel("Pan", (float)relativePan);
        fixture.GetChannels().OptionalSetChannel("Tilt", (float)relativeTilt);
	}
	
	private double GetDifference(double firstPan, double firstTilt, double secondPan, double secondTilt)
	{
	    return Math.abs((secondPan - firstPan) /*+ (secondTilt - firstTilt)*/);
	}
	
	private TreeMap<Double, Double> GetAlternatives(double pan, double tilt)
	{
	    TreeMap<Double, Double> alternatives = new TreeMap<>();
	    while(pan >= panMin)
	    {
	        pan -= 180f;
	        tilt = -tilt;
	    }
	    
	    do
        {
	        alternatives.put(pan, tilt);
	        pan += 180f;
	        tilt = -tilt;
        } 
	    while(pan <= panMax);
	    
	    return alternatives;
	}
	
	private Point3D GetTarget()
	{
		Entry<Float, Point3D> before = lookAtTargets.floorEntry(lookAtTargetValue);
		Entry<Float, Point3D> after = lookAtTargets.ceilingEntry(lookAtTargetValue);
		
		//need to lerp between the two values
		float beforeDifference = Math.abs(lookAtTargetValue - before.getKey());
		float afterDifference = Math.abs(lookAtTargetValue - after.getKey());
		float range = afterDifference + beforeDifference;
		
		if(range == 0.0f)
		{
		    return before.getValue();
		}
		
		float percentOfSecond = lookAtTargetValue / range;
		float percentOfFirst = 1 - percentOfSecond;
		
		Point3D firstInfluence = before.getValue().multiply(percentOfFirst);
		Point3D secondInfluence = after.getValue().multiply(percentOfSecond);
		
		return firstInfluence.add(secondInfluence);
	}
	
	public void SetSource(float x, float y, float z)
	{
	    source = new Point3D(x, y, z);
	}

	@Override
	public void Set(String attributeName, float value)
	{
		switch(attributeName)
		{
		case "LookAtTarget":
			lookAtTargetValue = value;
		}
	}

    @Override
    public String GetType()
    {
        return "lookAt";
    }

    @Override
    public Collection<? extends Attribute> GetAttributes()
    {
        ArrayList<Attribute> attributes = new ArrayList<>();
        attributes.add(new Attribute("x", source.getX() + ""));
        attributes.add(new Attribute("y", source.getY() + ""));
        attributes.add(new Attribute("z", source.getZ() + ""));
        return attributes;
    }
    
    @Override
    public boolean SolvesAttribute(String attribute)
    {
        return SolvesAttribute(attribute, solvableAttributes);
    }
    
    public static boolean GetIsApplicableToFixture(Fixture fixture)
    {
        return Solver.GetIsApplicableToFixture(fixture, solvableChannels);
    }

    @Override
    public float GetAttribute(String name)
    {
        return lookAtTargetValue;
    }

}
